﻿using System;
using System.Collections;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Framework.Metadata;
using Framework.Extensions;

namespace Framework.Sql
{
    public class ExpressionToSqlGenerator
    {
        private Expression RootExpression(Expression expression)
        {
            if (expression.NodeType == ExpressionType.Constant) return expression;

            var memberExpression = expression as MemberExpression;
            if (memberExpression != null) return RootExpression(memberExpression.Expression);

            var methodCallExpression = expression as MethodCallExpression;
            if (methodCallExpression != null)
            {
                if (methodCallExpression.Object == null && methodCallExpression.Arguments.Count == 1)
                    return RootExpression(methodCallExpression.Arguments.Single());
                if (methodCallExpression.Object != null)
                    return RootExpression(methodCallExpression.Object);
                return expression;
            }

            return expression;
        }

        private static string FormatForSql(object value)
        {
            if (value.GetType() == typeof(string))
                return string.Format("'{0}'", value);
            return value.ToString();
        }

        private string GetElementSql(Expression expression)
        {
            var rootExpression = RootExpression(expression);
            if (rootExpression.NodeType == ExpressionType.Constant)
                return FormatForSql(GetValue(expression));

            return GetSql("GetElementSql", expression);
        }

        private string GetSql(string methodPrefix, Expression expression)
        {
            var methodToInvoke = GetType()
                .GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
                .Single(method => method.Name.StartsWith(methodPrefix)
                                  && method.Name != methodPrefix
                                  && method.GetParameters().Single().ParameterType.IsAssignableFrom(expression.GetType()));
            return methodToInvoke.Invoke(this, new[] {expression}).ToString();
        }

        private string GetClauseSql(Expression expression)
        {
            return GetSql("GetClauseSql", expression);
        }

        private object GetValue(Expression expression)
        {
            return Expression.Lambda(expression).Compile().DynamicInvoke();
        }

        private string GetClauseSqlForMethodCall(MethodCallExpression methodCallExpression)
        {
            if (methodCallExpression.Method.Name == "Contains")
            {
                var containsArgument = methodCallExpression.Arguments.Single();
                var column = GetElementSql(containsArgument);
                var list = ((IEnumerable)GetValue(methodCallExpression.Object))
                    .Cast<object>()
                    .Select(o => o.ToString())
                    .SeparateBy(",");

                return string.Format("{0} in ({1})", column, list);
            }
            throw new NotImplementedException(string.Format("The requested method '{0}' was not implemented", methodCallExpression.Method.Name));
        }

        private string GetElementSqlForMemberExpression(MemberExpression memberExpression)
        {
            var propertyInfo = memberExpression.Member as PropertyInfo;
            if (propertyInfo == null) throw new NotImplementedException("Not supported");

            var metadata = PropertyMetadataBase.FromProperty(propertyInfo) as ColumnPropertyMetadata;
            if (metadata == null) throw new NotImplementedException("Not supported");

            return metadata.ColumnName;
        }

        private string GetClauseSqlForBinaryExpression(BinaryExpression binaryExpression)
        {
            if (binaryExpression.NodeType == ExpressionType.AndAlso)
                return string.Format("({0}) and ({1})", GetClauseSql(binaryExpression.Left), GetClauseSql(binaryExpression.Right));
            if (binaryExpression.NodeType == ExpressionType.Equal)
                return string.Format("{0} = {1}", GetElementSql(binaryExpression.Left), GetElementSql(binaryExpression.Right));

            throw new NotImplementedException(string.Format("Binary Expression '{0}' not supported", binaryExpression.NodeType));
        }

        private string GetElementSqlForConstantExpression(ConstantExpression constantExpression)
        {
            return constantExpression.Value.ToString();
        }

        public void GetSql(SqlBuilderBase sqlBuilder, LambdaExpression expression)
        {
            var sql = GetClauseSql(expression.Body);
            sqlBuilder.AddCondition(sql);
        }
    }
}